const EventEmitter = require('events').EventEmitter
const SerialPort = require('serialport')
const ModbusRTUEx = require('./modbus_zigbee')
require('./rtubufferdport_zigbee')
const bitwise = require('bitwise')
const os = require('os');

// constants
const kMaxSampleValue = 65536
/**
 * the class will trigger data event when read samples from modbus
 * the parameter of event is the sample array
 */
class ModbusManager extends EventEmitter {

  constructor(cfg, logger, msgClient, monitor) {
    super()
    this._logger = logger
    this._msgClient = msgClient
    this._cfg = cfg
    this._monitor = monitor;
    this._deviceId = cfg.deviceId
    this._samplingInterval = cfg.samplingInterval * 1000
    this._queryDelay = parseInt(cfg.queryDelay)
    this._useZigBee = cfg.useZigBee
    this._hostAddress = cfg.hostAddress
    this._moduleIds = [] // module array
    this._validChannelSet = new Map()
    this._inputModeSet = new Map()
    this._rangeSet = new Map()

    for (const module of cfg.modules) {
      const id = module.id
      const dstAddress = module.dstAddress
      this._moduleIds.push({
        id: id,
        dstAddress: dstAddress
      })
      let validChannels = Array(16).fill(0)
      let inputModes = Array(16).fill(0)
      let ranges = Array(12).fill({
        start: 0,
        slope: 0
      })
      for (const channel of module.channels) {
        const idx = Number.parseInt(channel.id) - 1 // channel id 1 based
        validChannels[idx] = 1
        inputModes[idx] = (channel.mode === 'AC') ? 0 : 1
        const min = Number.parseFloat(channel.outputValueMin)
        const max = Number.parseFloat(channel.outputValueMax)
        ranges[idx].start = min
        ranges[idx].slope = (max - min) / kMaxSampleValue
      }
      this._validChannelSet.set(id, validChannels)
      this._inputModeSet.set(id, inputModes)
      this._rangeSet.set(id, ranges)
    }

    this._port = null
    this._client = new ModbusRTUEx()
    this._searchInterval = null
    this._updateInterval = null
    this._commName = ''
    this._isOpened = false
    this._debug = true

    this.guardFn()
  }

  // /////////////////////////////////////////////////////////
  // Private methods
  // /////////////////////////////////////////////////////////

  // log info message and pass info message to msgClient if
  // it exists
  info(msg) {
    this._logger.info(msg);
    this._msgClient.send(msg);
  }

  // log error message and pass error message to msgClient if
  // it exists
  error(msg) {
    this._logger.error(msg);
    this._msgClient.send(msg);
  }

  findDasPort(callback) {
    SerialPort.list((err, ports) => {
      if (err) {
        this.error(err);
      } else {
        for (var i = 0; i < ports.length; i++) {
          if (ports[i].comName === this._cfg.comName) {
            this._commName = ports[i].comName
            break
          }
        }
        callback(this._commName)
      }
    })
  }

  createClient() {
    if (!this._isOpened) {
      this.findDasPort(comName => {
        if (comName) {
          this.info(`comName: ${comName}`);
          this._client.connectRTUBufferedEx(comName, this._useZigBee, {
            baudRate: this._cfg.baudRate,
            dataBits: 8,
            stopBits: 1,
            parity: 'even'
          }, (err) => {
            this.run(err)
          })
        } else {
          this.info('Could not find DAS serial port')
        }
      })
    }
  }

  // the function will be run repeatedly, to make sure the functionality of the module
  // works as expected
  guardFn() {
    this.createClient()
    setTimeout(() => {
      this.guardFn()
    }, 5000)
  }

  async run(err) {
    if (err) {
      this.error(`open serialport failed: ${err}`)
      return
    }
    this._isOpened = true
    this._client.setTimeout(this._queryDelay)

    this._client._port._client.on('close', () => {
      clearInterval(this._updateInterval)
      this._isOpened = false
    })

    let timeout = 0
    for (const md of this._moduleIds) {
      try {
        await this.setTargetAddr(md.dstAddress)
        let data = await this.readConfig(md.id, md.dstAddress)
        this.parseConfig(data)
      } catch (err) {
        this.handleError(err)
      }
    }
    setTimeout(() => {
      this._updateInterval = setInterval(() => this.getSamples(), this._samplingInterval)
    }, timeout)
  }

  /**
   * Set target collector zigbee <address>
   * @param dstAddress {number} the address of target zigbee endpoint
   </address>
   */
  setTargetAddr(dstAddress) {
    dstAddress = (this._useZigBee) ? dstAddress : undefined
    if (!dstAddress) {
      return Promise.resolve()
    }
    return new Promise((resolve, reject) => {
      this._client.writeDstAddress(dstAddress, (err) => {
        if (err) {
          //reject(err)
          resolve();
        } else {
          resolve();
        }
      })
    })
  }

  /**
   * Send Modbus Command to read config
   * @param id {number} target id of modbus endpoint, used for 485 connection
   * @param dstAddress {number} the address of target zigbee endpoint, used for zigbee connection
   */
  readConfig(id, dstAddress) {
    return new Promise((resolve, reject) => {
      let dataAddr = 0x0C
      let dataLen = 19
      let srcAddress = (this._useZigBee) ? this._hostAddress : id
      dstAddress = (this._useZigBee) ? dstAddress : undefined
      let attemptCnt = 0
      let writeFn = () => {
        this._client.writeFC3(srcAddress, dstAddress, dataAddr, dataLen, (err, data) => {
          if (err) {
            if (err === 'Timed out' && attemptCnt < this._cfg.retryCount) {
              this.info(`readConfig try again: ${attemptCnt}`);
              attemptCnt++;
              return writeFn();
            }
            resolve('');
          } else {
            resolve(data);
          }
        });
      };
      writeFn();
    })
  }

  parseConfig(data) {
    if (!data) return;
    const txtParity = ['无校验', ' 奇校验', '偶校验']
    const txtBaudRate = ['300bps', '600bps', '1200bps', '2400bps', '4800bps', '9600bps', '14400bps', '19200bps', '38400bps', '57600bps', '115200bps']

    let buffer = data.buffer
    let cfgInfo = '';
    // PanId
    this._panId = buffer.readUInt16BE()
    cfgInfo += 'Zigbee PanID: 0x' + this._panId.toString(16);
    cfgInfo += os.EOL;
    buffer = buffer.slice(2)

    // zigbee local addr
    this._localAddr = buffer.readUInt16BE()
    cfgInfo += 'Zigbee 本机地址: 0x' + this._localAddr.toString(16);
    cfgInfo += os.EOL;
    buffer = buffer.slice(2)

    // zigbee target addr
    this._targetAddr = buffer.readUInt16BE()
    cfgInfo += 'Zigbee 目标地址: 0x' + this._targetAddr.toString(16);
    cfgInfo += os.EOL;
    buffer = buffer.slice(2)

    // zigbee channel
    let chanBuf = buffer.slice(0, 2).swap16()
    this._channelId = chanBuf[0] & 0x7F
    this._zigbeeActive = (chanBuf[0] >>> 7) & 0x01
    cfgInfo += 'Zigbee 通道号: ' + this._channelId;
    cfgInfo += os.EOL;
    cfgInfo += 'Zigbee 状态: ' + (this._zigbeeActive ? '激活' : '没激活');
    cfgInfo += os.EOL;
    buffer = buffer.slice(2)

    let settingBuf = buffer.slice(0, 2).swap16()
    let devId = settingBuf[1]
    let baudRate = settingBuf[0] & 0xF
    let parity = (settingBuf[0] >>> 4) & 0x3
    cfgInfo += '设备ID:　' + devId;
    cfgInfo += os.EOL;
    cfgInfo += '波特率: ' + txtBaudRate[baudRate];
    cfgInfo += os.EOL;
    cfgInfo += '校验: ' + txtParity[parity];
    cfgInfo += os.EOL;
    buffer = buffer.slice(6)
    let inputModes = bitwise.readBuffer(buffer, 0, 16).reverse()
    for (let index = 0; index < inputModes.length; index++) {
      let item = inputModes[index]
      let value = (item) ? '直流' : '交流'
      cfgInfo += `通道[${index + 1}]: ${value}`;
      cfgInfo += os.EOL;
    }
    buffer = buffer.slice(4) // skip 0x14

    const referenceValue = buffer.readUInt16BE()
    cfgInfo += `参考电压:  ${referenceValue}mV`;
    cfgInfo += os.EOL;
    buffer = buffer.slice(2)

    const version = buffer.readUInt16BE()
    cfgInfo += `版本: ${version}`;
    cfgInfo += os.EOL;
    buffer = buffer.slice(2)

    buffer.swap16()
    const productId = buffer.toString('hex', 0, 12)
    cfgInfo += `序列号: ${productId}`;
    cfgInfo += os.EOL;
    buffer = buffer.slice(12)

    const vendorId = buffer.toString()
    cfgInfo += `产品型号: ${vendorId}`;
    cfgInfo += os.EOL;
    this.info(cfgInfo);
  }

  modifyInputMode(id, dstAddress) {
    return new Promise((resolve, reject) => {
      var buf = this._inputModeSet.get(id)
      var num = bitwise.createBuffer(buf.reverse()).readUInt16BE()
      const srcAddress = (this._useZigBee) ? this._hostAddress : id
      dstAddress = (this._useZigBee) ? dstAddress : undefined
      this._client.writeFC6(srcAddress, dstAddress, 0x13, num, (err, data) => {
        if (err) {
          reject(err)
        } else {
          resolve(data)
        }
      })
    })
  }

  /**
   * Send Modbus Command to read ADC samples
   * @param id {number} target id of modbus endpoint, used for 485 connection
   * @param dstAddress {number} the address of target zigbee endpoint, used for zigbee connection
   */
  readSamples(id, dstAddress) {
    this._monitor.blinkCollector();
    // TODO: send all zeros to server, needs to redefine this value
    const errData = {
      //data: Buffer.from([0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0])
      data: Array(12).fill(-1) // negative value to present invalid data
    }
    let attemptCnt = 0
    return new Promise((resolve, reject) => {
      const srcAddress = (this._useZigBee) ? this._hostAddress : id
      dstAddress = (this._useZigBee) ? dstAddress : undefined
      let writeFn = () => {
        this._client.writeFC3(srcAddress, dstAddress, 0x20, 12, (err, data) => {
          if (err) {
            if (err === 'Timed out' && attemptCnt < this._cfg.retryCount) {
              this.info(`readSamples try again: ${attemptCnt}`)
              attemptCnt++
              return writeFn()
            }
            resolve(errData)
          } else {
            resolve(data)
          }
        })
      };

      writeFn()
    })
  }

  parseSamples(id, data) {
    let validChannels = this._validChannelSet.get(id)
    let range = this._rangeSet.get(id)
    for (let idx = 0; idx < validChannels.length; idx++) {
      if (validChannels[idx]) {
        const raw = data.data[idx]
        let val = range[idx].start + raw * range[idx].slope
        val = val.toFixed(this._cfg.fixedDigits)
        // construct key has format like this '001-01-01'
        let key = ('00' + this._deviceId).slice(-3)
        key += '-'
        key += ('0' + id).slice(-2)
        key += '-'
        key += ('0' + (idx + 1)).slice(-2)
        // this.samples.push(`${key}:${val}`)
        this.samples.push(`${key}:${raw}`)
      }
    }
  }


  async getSamples() {
    try {
      this.samples = []
      for (const md of this._moduleIds) {
        await this.setTargetAddr(md.dstAddress)
        let data = await this.readSamples(md.id, md.dstAddress)
        this.parseSamples(md.id, data)
      }

      const msg = `${this.samples}`
      this.emit('data', msg)
    } catch (err) {
      this.handleError(err)
    }
  }

  handleError(err) {
    this.error(err)
    clearInterval(this._updateInterval)
    if (this._isOpened) {
      this._client.close()
    }
    this._isOpened = false
  }
}

module.exports = ModbusManager

if (require.main === module) {
  const modbusMgr = new ModbusManager()
}
